#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <shellapi.h>
#include <dwmapi.h>

#ifndef DWMWA_CLOAK
#define DWMWA_CLOAK 14
#endif

#define sb_free(a) ((a) ? HeapFree(GetProcessHeap(), 0, stb__sbraw(a)), 0 : 0)
#define sb_push(a, v) (stb__sbmaybegrow(a, 1), (a)[stb__sbn(a)++] = (v))
#define sb_count(a) ((a) ? stb__sbn(a) : 0)

#define stb__sbraw(a) ((int *)(a)-2)
#define stb__sbm(a) stb__sbraw(a)[0]
#define stb__sbn(a) stb__sbraw(a)[1]

#define stb__sbneedgrow(a, n) ((a) == 0 || stb__sbn(a) + (n) >= stb__sbm(a))
#define stb__sbmaybegrow(a, n) (stb__sbneedgrow(a, (n)) ? stb__sbgrow(a, n) : 0)
#define stb__sbgrow(a, n) ((a) = stb__sbgrowf((a), (n), sizeof(*(a))))

#ifndef MOD_NOREPEAT
#define MOD_NOREPEAT 0x4000
#endif

#define NUM_DESKTOPS 4
#define MAX_MONITORS 8
#define WM_TRAYICON (WM_APP + 1)
#define ID_TRAY_TOGGLE_TASKBAR 1000
#define ID_TRAY_CONFIG 1001
#define ID_TRAY_EXIT 1002

#define IDC_SWITCH_BASE 200
#define IDC_MOVE_BASE (IDC_SWITCH_BASE + NUM_DESKTOPS)
#define IDC_PREV 280
#define IDC_NEXT 281
#define IDC_APPLY 300
#define IDC_CLOSE 301

#define HOTKEY_ID_SWITCH_BASE 0
#define HOTKEY_ID_MOVE_BASE NUM_DESKTOPS
#define HOTKEY_ID_SPECIAL_BASE (NUM_DESKTOPS * 2)
#define HOTKEY_ID_EXIT (HOTKEY_ID_SPECIAL_BASE)
#define HOTKEY_ID_TOGGLE (HOTKEY_ID_SPECIAL_BASE + 1)
#define HOTKEY_ID_PREV (HOTKEY_ID_SPECIAL_BASE + 2)
#define HOTKEY_ID_NEXT (HOTKEY_ID_SPECIAL_BASE + 3)

#define CAPTURE_TYPE_NONE 0
#define CAPTURE_TYPE_SWITCH 1
#define CAPTURE_TYPE_MOVE 2
#define CAPTURE_TYPE_PREV 3
#define CAPTURE_TYPE_NEXT 4

typedef struct {
	unsigned modifiers;
	unsigned vk;
} Hotkey;

typedef struct {
	HWND *windows;
	unsigned count;
} Windows;

struct Virgo;
typedef struct Virgo Virgo;

typedef struct {
	NOTIFYICONDATA nid;
	HBITMAP hBitmap;
	HFONT hFont;
	HWND hwnd;
	HDC mdc;
	unsigned bitmapWidth;
	Virgo *virgo;
} Trayicon;

typedef struct {
	Windows desktops[NUM_DESKTOPS];
	unsigned current;
	HMONITOR monitor;
	DWORD suppress_foreground_until;
	HWND last_active[NUM_DESKTOPS];
	HWND *z_order[NUM_DESKTOPS];
} MonitorState;

struct Virgo {
	unsigned monitor_count;
	unsigned handle_hotkeys;
	MonitorState monitors[MAX_MONITORS];
	Trayicon trayicon;
	char config_path[MAX_PATH];
	Hotkey hotkeys_switch[NUM_DESKTOPS];
	Hotkey hotkeys_move[NUM_DESKTOPS];
	Hotkey hotkey_prev;
	Hotkey hotkey_next;
	Hotkey pending_switch[NUM_DESKTOPS];
	Hotkey pending_move[NUM_DESKTOPS];
	Hotkey pending_prev;
	Hotkey pending_next;
	unsigned pending_dirty;
	unsigned show_all_taskbar;
	HWINEVENTHOOK foreground_hook;
	unsigned suppress_foreground;
	HWND config_window;
	unsigned capturing_type;
	unsigned capturing_index;
	HWND switch_buttons[NUM_DESKTOPS];
	HWND move_buttons[NUM_DESKTOPS];
	HWND button_prev;
	HWND button_next;
};

static unsigned is_valid_window(HWND hwnd);
static void trayicon_refresh(Virgo *v);
static void virgo_set_show_all_taskbar(Virgo *v, unsigned enabled);
static void virgo_show_config_window(Virgo *v);
static void virgo_unregister_all_hotkeys(Virgo *v);
static unsigned virgo_register_all_hotkeys(Virgo *v);
static void virgo_apply_all_visibility(Virgo *v);
static void virgo_apply_desktop_visibility(Virgo *v, MonitorState *state,
		unsigned desk_idx, unsigned active, HWND preferred);
static void virgo_collect_desk_windows(Virgo *v, MonitorState *state,
	unsigned desk_idx, HWND **out_windows);
static void virgo_restore_zorder(HWND *windows, unsigned count);
static unsigned virgo_hwnd_in_list(HWND *windows, unsigned count, HWND hwnd);
static int virgo_find_window(Virgo *v, HWND hwnd, unsigned *monitor_index,
						unsigned *desk_index);
static void virgo_switch_monitor_to_desk(Virgo *v, unsigned monitor_idx,
			unsigned desk, HWND preferred);
static void virgo_step_desktop(Virgo *v, int delta);
static void virgo_activate_window(HWND hwnd);
static unsigned register_hotkey(unsigned id, unsigned mod, unsigned vk);
static LRESULT CALLBACK trayicon_wndproc(HWND hwnd, UINT msg, WPARAM wParam,
						LPARAM lParam);
static void trayicon_show_menu(Virgo *v, POINT pt);
static void trayicon_handle_command(Virgo *v, unsigned command);
static void virgo_update_foreground_hook(Virgo *v);
static void CALLBACK virgo_foreground_proc(HWINEVENTHOOK hook, DWORD event,
						HWND hwnd, LONG idObject, LONG idChild,
						DWORD idEventThread, DWORD dwmsEventTime);
static void virgo_init_default_hotkeys(Virgo *v);
static void virgo_update_config_button_text(Virgo *v, unsigned type,
						unsigned index);
static void virgo_update_all_config_text(Virgo *v);
static void virgo_begin_capture(Virgo *v, unsigned type, unsigned index);
static void virgo_cancel_capture(Virgo *v);
static void virgo_apply_configured_hotkeys(Virgo *v);
static UINT virgo_get_modifier_state(void);
static LRESULT CALLBACK config_wndproc(HWND hwnd, UINT msg, WPARAM wParam,
						LPARAM lParam);
static void virgo_hotkey_to_string(const Hotkey *hk, char *buffer,
						unsigned buffer_size);
static void virgo_sync_pending_from_active(Virgo *v);
static void virgo_append_text(char *buffer, unsigned buffer_size,
					   const char *text);
static void virgo_append_number(char *buffer, unsigned buffer_size,
					     unsigned value);
static void virgo_init_config_path(Virgo *v);
static void virgo_load_config(Virgo *v);
static void virgo_save_config(Virgo *v);
static void virgo_process_config_line(Virgo *v, const char *line);
static unsigned virgo_parse_uint(const char *text, const char **endptr,
				     unsigned *ok);
static unsigned virgo_string_equals(const char *a, const char *b);
static unsigned virgo_string_starts_with(const char *text,
				      const char *prefix);
static void virgo_dwm_init(void);
static void virgo_dwm_deinit(void);
static unsigned virgo_set_window_cloak(HWND hwnd, unsigned cloaked);
static UINT_PTR virgo_get_window_state(HWND hwnd);
static void virgo_set_window_state(HWND hwnd, UINT_PTR state);

static Virgo *g_virgo;
static HMODULE g_dwmapi;
typedef HRESULT(WINAPI *DwmSetWindowAttributeProc)(HWND, DWORD, LPCVOID, DWORD);
static DwmSetWindowAttributeProc g_pDwmSetWindowAttribute;
static const char VIRGO_PROP_STATE[] = "VirgoWindowState";
#define VIRGO_STATE_TRACKED 0x01u
#define VIRGO_STATE_WAS_ICONIC 0x02u
#define VIRGO_STATE_FORCED_MINIMIZED 0x04u
#define VIRGO_STATE_CLOAKED 0x08u

static void memclr(void *ptr, unsigned size)
{
	unsigned i;
	unsigned char *p;
	p = (unsigned char *)ptr;
	for (i = 0; i < size; i++) {
		p[i] = 0;
	}
}

static void *stb__sbgrowf(void *arr, unsigned increment, unsigned itemsize)
{
	unsigned dbl_cur = arr ? 2 * stb__sbm(arr) : 0;
	unsigned min_needed = sb_count(arr) + increment;
	unsigned m = dbl_cur > min_needed ? dbl_cur : min_needed;
	unsigned *p;
	if (arr) {
		p = HeapReAlloc(GetProcessHeap(), 0, stb__sbraw(arr),
						itemsize * m + sizeof(unsigned) * 2);
	} else {
		p = HeapAlloc(GetProcessHeap(), 0, itemsize * m + sizeof(unsigned) * 2);
	}
	if (p) {
		if (!arr) {
			p[1] = 0;
		}
		p[0] = m;
		return p + 2;
	} else {
		ExitProcess(1);
		return (void *)(2 * sizeof(unsigned));
	}
}

static void virgo_dwm_init(void)
{
	if (g_dwmapi) {
		return;
	}
	g_dwmapi = LoadLibraryA("dwmapi.dll");
	if (!g_dwmapi) {
		return;
	}
	g_pDwmSetWindowAttribute =
		(DwmSetWindowAttributeProc)GetProcAddress(g_dwmapi, "DwmSetWindowAttribute");
	if (!g_pDwmSetWindowAttribute) {
		FreeLibrary(g_dwmapi);
		g_dwmapi = NULL;
	}
}

static void virgo_dwm_deinit(void)
{
	g_pDwmSetWindowAttribute = NULL;
	if (g_dwmapi) {
		FreeLibrary(g_dwmapi);
		g_dwmapi = NULL;
	}
}

static unsigned virgo_set_window_cloak(HWND hwnd, unsigned cloaked)
{
	BOOL enable;
	if (!g_pDwmSetWindowAttribute || !hwnd || !IsWindow(hwnd)) {
		return 0;
	}
	enable = cloaked ? TRUE : FALSE;
	return SUCCEEDED(g_pDwmSetWindowAttribute(hwnd, DWMWA_CLOAK, &enable,
				sizeof(enable))) ? 1u : 0u;
}

static UINT_PTR virgo_get_window_state(HWND hwnd)
{
	return (UINT_PTR)GetPropA(hwnd, VIRGO_PROP_STATE);
}

static void virgo_set_window_state(HWND hwnd, UINT_PTR state)
{
	if (!hwnd || !IsWindow(hwnd)) {
		return;
	}
	if (state) {
		SetPropA(hwnd, VIRGO_PROP_STATE, (HANDLE)state);
	} else {
		RemovePropA(hwnd, VIRGO_PROP_STATE);
	}
}

static HICON trayicon_draw(Trayicon *t, char *text, unsigned len)
{
	ICONINFO iconInfo;
	HBITMAP hOldBitmap;
	HFONT hOldFont;
	SIZE textSize;
	int x;
	int y;
	hOldBitmap = (HBITMAP)SelectObject(t->mdc, t->hBitmap);
	hOldFont = (HFONT)SelectObject(t->mdc, t->hFont);
	PatBlt(t->mdc, 0, 0, t->bitmapWidth, t->bitmapWidth, BLACKNESS);
	GetTextExtentPoint32(t->mdc, text, len, &textSize);
	x = (t->bitmapWidth - textSize.cx) / 2;
	if (x < 0) {
		x = 0;
	}
	y = (t->bitmapWidth - textSize.cy) / 2;
	if (y < 0) {
		y = 0;
	}
	TextOut(t->mdc, x, y, text, len);
	SelectObject(t->mdc, hOldBitmap);
	SelectObject(t->mdc, hOldFont);
	iconInfo.fIcon = TRUE;
	iconInfo.xHotspot = iconInfo.yHotspot = 0;
	iconInfo.hbmMask = iconInfo.hbmColor = t->hBitmap;
	return CreateIconIndirect(&iconInfo);
}

static void trayicon_init(Virgo *v)
{
	HDC hdc;
	WNDCLASSA wc;
	static ATOM tray_atom;
	Trayicon *t;
	t = &v->trayicon;
	if (!tray_atom) {
		memclr(&wc, sizeof(wc));
		wc.lpfnWndProc = &trayicon_wndproc;
		wc.hInstance = GetModuleHandle(NULL);
		wc.lpszClassName = "VirgoTrayWindow";
		tray_atom = RegisterClassA(&wc);
		if (!tray_atom) {
			MessageBox(NULL, "could not register tray window", "error",
					   MB_ICONEXCLAMATION);
			ExitProcess(1);
		}
	}
	t->virgo = v;
	t->hwnd = CreateWindowExA(WS_EX_TOOLWINDOW, "VirgoTrayWindow",
				     "virgo_tray", WS_OVERLAPPED, 0, 0, 0, 0, NULL,
				     NULL, GetModuleHandle(NULL), v);
	if (!t->hwnd) {
		MessageBox(NULL, "could not create tray window", "error",
				   MB_ICONEXCLAMATION);
		ExitProcess(1);
	}
	t->bitmapWidth = GetSystemMetrics(SM_CXSMICON);
	t->nid.cbSize = sizeof(t->nid);
	t->nid.hWnd = t->hwnd;
	t->nid.uID = 100;
	t->nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
	t->nid.uCallbackMessage = WM_TRAYICON;
	hdc = GetDC(t->hwnd);
	t->hBitmap = CreateCompatibleBitmap(hdc, t->bitmapWidth, t->bitmapWidth);
	t->mdc = CreateCompatibleDC(hdc);
	ReleaseDC(t->hwnd, hdc);
	SetBkColor(t->mdc, RGB(0x00, 0x00, 0x00));
	SetTextColor(t->mdc, RGB(0x00, 0xFF, 0x00));
	t->hFont = CreateFont(-MulDiv(11, GetDeviceCaps(t->mdc, LOGPIXELSY), 72), 0,
					 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, TEXT("Arial"));
	memclr(t->nid.szTip, sizeof(t->nid.szTip));
	lstrcpynA(t->nid.szTip, "Virgo Desktop", sizeof(t->nid.szTip));
	t->nid.hIcon = trayicon_draw(t, "1", 1);
	Shell_NotifyIcon(NIM_ADD, &t->nid);
}

static void trayicon_set(Trayicon *t, char *text, unsigned len)
{
	DestroyIcon(t->nid.hIcon);
	t->nid.hIcon = trayicon_draw(t, text, len);
	Shell_NotifyIcon(NIM_MODIFY, &t->nid);
}

static void trayicon_deinit(Trayicon *t)
{
	Shell_NotifyIcon(NIM_DELETE, &t->nid);
	DestroyIcon(t->nid.hIcon);
	DeleteObject(t->hBitmap);
	DeleteObject(t->hFont);
	DeleteDC(t->mdc);
	if (t->hwnd) {
		DestroyWindow(t->hwnd);
	}
	t->hwnd = NULL;
	t->virgo = NULL;
}

static void trayicon_handle_command(Virgo *v, unsigned command)
{
	if (!v) {
		return;
	}
	if (command == ID_TRAY_TOGGLE_TASKBAR) {
		virgo_set_show_all_taskbar(v, !v->show_all_taskbar);
	} else if (command == ID_TRAY_CONFIG) {
		virgo_show_config_window(v);
	} else if (command == ID_TRAY_EXIT) {
		PostQuitMessage(0);
	}
}

static void trayicon_show_menu(Virgo *v, POINT pt)
{
	HMENU menu;
	unsigned flags;
	unsigned command;
	menu = CreatePopupMenu();
	if (!menu) {
		return;
	}
	flags = MF_BYPOSITION | MF_STRING;
	if (v->show_all_taskbar) {
		flags |= MF_CHECKED;
	}
	InsertMenu(menu, (UINT)-1, flags, ID_TRAY_TOGGLE_TASKBAR,
		   "Show windows from all desktops");
	InsertMenu(menu, (UINT)-1, MF_BYPOSITION | MF_STRING, ID_TRAY_CONFIG,
		   "Configure hotkeys...");
	InsertMenu(menu, (UINT)-1, MF_BYPOSITION | MF_SEPARATOR, 0, NULL);
	InsertMenu(menu, (UINT)-1, MF_BYPOSITION | MF_STRING, ID_TRAY_EXIT,
		   "Exit");
	SetForegroundWindow(v->trayicon.hwnd);
	command = TrackPopupMenu(menu,
				TPM_RETURNCMD | TPM_NONOTIFY | TPM_RIGHTBUTTON,
				pt.x, pt.y, 0, v->trayicon.hwnd, NULL);
	DestroyMenu(menu);
	if (command) {
		trayicon_handle_command(v, command);
	}
}

static LRESULT CALLBACK trayicon_wndproc(HWND hwnd, UINT msg, WPARAM wParam,
						LPARAM lParam)
{
	Virgo *v;
	if (msg == WM_CREATE) {
		CREATESTRUCT *cs;
		cs = (CREATESTRUCT *)lParam;
		SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)cs->lpCreateParams);
		return 0;
	}
	v = (Virgo *)GetWindowLongPtr(hwnd, GWLP_USERDATA);
	switch (msg) {
	case WM_TRAYICON: {
		if (!v) {
			break;
		}
		if (lParam == WM_RBUTTONUP || lParam == WM_CONTEXTMENU) {
			POINT pt;
			GetCursorPos(&pt);
			trayicon_show_menu(v, pt);
		}
		break;
	}
	case WM_COMMAND: {
		if (v) {
			trayicon_handle_command(v, LOWORD(wParam));
		}
		break;
	}
	case WM_DESTROY:
		SetWindowLongPtr(hwnd, GWLP_USERDATA, 0);
		return 0;
	}
	return DefWindowProc(hwnd, msg, wParam, lParam);
}

typedef struct {
	Virgo *virgo;
	unsigned index;
} MonitorEnumContext;

static BOOL CALLBACK monitor_enum_proc(HMONITOR monitor, HDC hdc, LPRECT rect,
								LPARAM lParam)
{
	MonitorEnumContext *ctx;
	MonitorState *state;
	(void)hdc;
	(void)rect;
	ctx = (MonitorEnumContext *)lParam;
	if (ctx->index >= MAX_MONITORS) {
		return FALSE;
	}
	state = &ctx->virgo->monitors[ctx->index];
	memclr(state, sizeof(*state));
	state->monitor = monitor;
	ctx->index++;
	ctx->virgo->monitor_count = ctx->index;
	return TRUE;
}

static void virgo_init_monitors(Virgo *v)
{
	MonitorEnumContext ctx;
	POINT pt;
	memclr(&ctx, sizeof(ctx));
	ctx.virgo = v;
	EnumDisplayMonitors(NULL, NULL, monitor_enum_proc, (LPARAM)&ctx);
	if (!v->monitor_count) {
		MonitorState *state = &v->monitors[0];
		memclr(state, sizeof(*state));
		pt.x = 0;
		pt.y = 0;
		state->monitor = MonitorFromPoint(pt, MONITOR_DEFAULTTOPRIMARY);
		v->monitor_count = 1;
	}
}

static unsigned virgo_monitor_index(Virgo *v, HMONITOR monitor)
{
	unsigned i;
	for (i = 0; i < v->monitor_count; i++) {
		if (v->monitors[i].monitor == monitor) {
			return i;
		}
	}
	return (unsigned)-1;
}

static unsigned virgo_monitor_from_hwnd(Virgo *v, HWND hwnd)
{
	HMONITOR monitor;
	unsigned idx;
	monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
	idx = virgo_monitor_index(v, monitor);
	if (idx == (unsigned)-1) {
		idx = 0;
	}
	return idx;
}

static unsigned virgo_contains_window(Virgo *v, HWND hwnd)
{
	unsigned monitor_idx;
	unsigned desk_idx;
	return virgo_find_window(v, hwnd, &monitor_idx, &desk_idx);
}

static int virgo_find_window(Virgo *v, HWND hwnd, unsigned *monitor_index,
						unsigned *desk_index)
{
	unsigned m, d, w;
	MonitorState *state;
	Windows *desk;
	for (m = 0; m < v->monitor_count; m++) {
		state = &v->monitors[m];
		for (d = 0; d < NUM_DESKTOPS; d++) {
			desk = &state->desktops[d];
			for (w = 0; w < desk->count; w++) {
				if (desk->windows[w] == hwnd) {
					if (monitor_index) {
						*monitor_index = m;
					}
					if (desk_index) {
						*desk_index = d;
					}
					return 1;
				}
			}
		}
	}
	return 0;
}

static void virgo_collect_desk_windows(Virgo *v, MonitorState *state,
	unsigned desk_idx, HWND **out_windows)
{
	HWND hwnd;
	if (!v || !state || !out_windows) {
		return;
	}
	*out_windows = NULL;
	hwnd = GetTopWindow(NULL);
	while (hwnd) {
		unsigned mon_idx;
		unsigned d_idx;
		if (virgo_find_window(v, hwnd, &mon_idx, &d_idx)) {
			if (mon_idx < v->monitor_count && &v->monitors[mon_idx] == state &&
			    d_idx == desk_idx) {
				sb_push(*out_windows, hwnd);
			}
		}
		hwnd = GetWindow(hwnd, GW_HWNDPREV);
	}
}

static void virgo_restore_zorder(HWND *windows, unsigned count)
{
	HWND *normal = NULL;
	HWND *topmost = NULL;
	unsigned i;
	if (!windows || !count) {
		return;
	}
	for (i = count; i-- > 0;) {
		HWND hwnd = windows[i];
		LONG ex;
		if (!hwnd || !IsWindow(hwnd)) {
			continue;
		}
		ex = GetWindowLong(hwnd, GWL_EXSTYLE);
		if (ex & WS_EX_TOPMOST) {
			sb_push(topmost, hwnd);
		} else {
			sb_push(normal, hwnd);
		}
	}
	if (sb_count(normal)) {
		HWND insertAfter = HWND_BOTTOM;
		UINT flags = SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE |
			SWP_NOOWNERZORDER;
		for (i = 0; i < sb_count(normal); i++) {
			HWND hwnd = normal[i];
			if (!IsWindow(hwnd)) {
				continue;
			}
			SetWindowPos(hwnd, insertAfter, 0, 0, 0, 0, flags);
			insertAfter = hwnd;
		}
	}
	if (sb_count(topmost)) {
		HWND insertAfter = HWND_TOPMOST;
		UINT flags = SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE |
			SWP_NOOWNERZORDER;
		HWND prev = NULL;
		for (i = sb_count(topmost); i-- > 0;) {
			HWND hwnd = topmost[i];
			HWND target;
			if (!IsWindow(hwnd)) {
				continue;
			}
			target = prev ? prev : insertAfter;
			SetWindowPos(hwnd, target, 0, 0, 0, 0, flags);
			prev = hwnd;
		}
	}
	sb_free(normal);
	sb_free(topmost);
}

static unsigned virgo_hwnd_in_list(HWND *windows, unsigned count, HWND hwnd)
{
	unsigned i;
	if (!windows || !count || !hwnd) {
		return 0;
	}
	for (i = 0; i < count; i++) {
		if (windows[i] == hwnd) {
			return 1;
		}
	}
	return 0;
}

static void trayicon_refresh(Virgo *v)
{
	char text[2 * MAX_MONITORS];
	unsigned pos = 0;
	unsigned i;
	if (!v->monitor_count) {
		text[pos++] = '1';
	} else {
		for (i = 0; i < v->monitor_count && pos < sizeof(text) - 1; i++) {
			text[pos++] = (char)('1' + v->monitors[i].current);
			if (i + 1 < v->monitor_count && pos < sizeof(text) - 1) {
				text[pos++] = '/';
			}
		}
	}
	text[pos] = 0;
	trayicon_set(&v->trayicon, text, pos);
}

static unsigned virgo_active_monitor(Virgo *v)
{
	HWND hwnd;
	unsigned idx;
	POINT pt;
	HMONITOR monitor;
	if (GetCursorPos(&pt)) {
		monitor = MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST);
		idx = virgo_monitor_index(v, monitor);
		if (idx != (unsigned)-1) {
			return idx;
		}
	}
	hwnd = GetForegroundWindow();
	if (hwnd && is_valid_window(hwnd)) {
		return virgo_monitor_from_hwnd(v, hwnd);
	}
	return 0;
}

static void virgo_set_window_visibility(Virgo *v, HWND hwnd, unsigned active)
{
	UINT flags;
	BOOL visible;
	BOOL iconic;
	UINT_PTR state;
	BOOL show_fallback;
	if (!hwnd || !IsWindow(hwnd)) {
		return;
	}
	flags = SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOOWNERZORDER |
		SWP_NOZORDER;
	state = virgo_get_window_state(hwnd);
	visible = IsWindowVisible(hwnd);
	iconic = IsIconic(hwnd);
	if (active) {
		if (state & VIRGO_STATE_CLOAKED) {
			virgo_set_window_cloak(hwnd, 0);
			state &= ~VIRGO_STATE_CLOAKED;
		}
		if (!visible) {
			show_fallback = !SetWindowPos(hwnd, NULL, 0, 0, 0, 0,
				flags | SWP_SHOWWINDOW);
			if (show_fallback) {
				ShowWindow(hwnd, (state & VIRGO_STATE_WAS_ICONIC) ?
					SW_SHOWMINNOACTIVE : SW_SHOWNOACTIVATE);
			}
		}
		if ((state & VIRGO_STATE_FORCED_MINIMIZED) && IsIconic(hwnd)) {
			ShowWindow(hwnd, (state & VIRGO_STATE_WAS_ICONIC) ?
				SW_SHOWMINNOACTIVE : SW_SHOWNOACTIVATE);
		}
		state &= ~VIRGO_STATE_FORCED_MINIMIZED;
		virgo_set_window_state(hwnd, state);
		return;
	}
	if (iconic) {
		state |= VIRGO_STATE_WAS_ICONIC;
		state &= ~VIRGO_STATE_FORCED_MINIMIZED;
		if (state & VIRGO_STATE_CLOAKED) {
			virgo_set_window_cloak(hwnd, 0);
			state &= ~VIRGO_STATE_CLOAKED;
		}
		virgo_set_window_state(hwnd, state);
		return;
	}
	state &= ~VIRGO_STATE_WAS_ICONIC;
	state |= VIRGO_STATE_TRACKED;
	if (v->show_all_taskbar) {
		if (!IsIconic(hwnd)) {
			ShowWindow(hwnd, SW_FORCEMINIMIZE);
		}
		state |= VIRGO_STATE_FORCED_MINIMIZED;
		if (state & VIRGO_STATE_CLOAKED) {
			virgo_set_window_cloak(hwnd, 0);
			state &= ~VIRGO_STATE_CLOAKED;
		}
	} else {
		if (visible) {
			if (!SetWindowPos(hwnd, NULL, 0, 0, 0, 0,
				flags | SWP_HIDEWINDOW)) {
				ShowWindow(hwnd, SW_HIDE);
			}
		}
		if (virgo_set_window_cloak(hwnd, 1)) {
			state |= VIRGO_STATE_CLOAKED;
		} else {
			state &= ~VIRGO_STATE_CLOAKED;
		}
		state &= ~VIRGO_STATE_FORCED_MINIMIZED;
	}
	virgo_set_window_state(hwnd, state);
}

static void virgo_activate_window(HWND hwnd)
{
	if (!hwnd || !IsWindow(hwnd)) {
		return;
	}
	if (IsIconic(hwnd)) {
		return;
	}
	ShowWindow(hwnd, SW_SHOW);
	SetForegroundWindow(hwnd);
	BringWindowToTop(hwnd);
	SetActiveWindow(hwnd);
}

static void virgo_apply_desktop_visibility(Virgo *v, MonitorState *state,
		unsigned desk_idx, unsigned active, HWND preferred)
{
	Windows *desk;
	HWND *order = NULL;
	HWND *collected = NULL;
	unsigned count = 0;
	unsigned i;
	if (!state) {
		return;
	}
	desk = &state->desktops[desk_idx];
	if (active) {
		order = state->z_order[desk_idx];
		if (!order) {
			virgo_collect_desk_windows(v, state, desk_idx, &collected);
			order = collected;
		}
		count = sb_count(order);
		if (count) {
			for (i = count; i-- > 0;) {
				HWND hwnd = order[i];
				if (preferred && hwnd == preferred) {
					continue;
				}
				virgo_set_window_visibility(v, hwnd, 1);
			}
		} else if (desk->count) {
			for (i = desk->count; i-- > 0;) {
				HWND hwnd = desk->windows[i];
				if (preferred && hwnd == preferred) {
					continue;
				}
				virgo_set_window_visibility(v, hwnd, 1);
			}
		}
		if (desk->count) {
			for (i = 0; i < desk->count; i++) {
				HWND hwnd = desk->windows[i];
				if (preferred && hwnd == preferred) {
					continue;
				}
				if (count && virgo_hwnd_in_list(order, count, hwnd)) {
					continue;
				}
				virgo_set_window_visibility(v, hwnd, 1);
			}
		}
		if (preferred) {
			virgo_set_window_visibility(v, preferred, 1);
		}
		if (count) {
			virgo_restore_zorder(order, count);
		}
		if (state->z_order[desk_idx]) {
			sb_free(state->z_order[desk_idx]);
			state->z_order[desk_idx] = NULL;
		}
		if (collected) {
			sb_free(collected);
		}
		return;
	}
	if (state->z_order[desk_idx]) {
		sb_free(state->z_order[desk_idx]);
		state->z_order[desk_idx] = NULL;
	}
	virgo_collect_desk_windows(v, state, desk_idx, &order);
	state->z_order[desk_idx] = order;
	count = sb_count(order);
	if (count) {
		for (i = 0; i < count; i++) {
			HWND hwnd = order[i];
			virgo_set_window_visibility(v, hwnd, 0);
		}
	} else {
		for (i = 0; i < desk->count; i++) {
			virgo_set_window_visibility(v, desk->windows[i], 0);
		}
	}
}

static void virgo_init_default_hotkeys(Virgo *v)
{
	unsigned i;
	for (i = 0; i < NUM_DESKTOPS; i++) {
		v->hotkeys_switch[i].modifiers = MOD_ALT;
		v->hotkeys_switch[i].vk = '1' + i;
		v->hotkeys_move[i].modifiers = MOD_CONTROL;
		v->hotkeys_move[i].vk = '1' + i;
		v->pending_switch[i] = v->hotkeys_switch[i];
		v->pending_move[i] = v->hotkeys_move[i];
	}
	v->hotkey_prev.modifiers = MOD_ALT;
	v->hotkey_prev.vk = VK_LEFT;
	v->hotkey_next.modifiers = MOD_ALT;
	v->hotkey_next.vk = VK_RIGHT;
	v->pending_prev = v->hotkey_prev;
	v->pending_next = v->hotkey_next;
	v->pending_dirty = 0;
}

static void virgo_unregister_all_hotkeys(Virgo *v)
{
	unsigned i;
	for (i = 0; i < NUM_DESKTOPS; i++) {
		UnregisterHotKey(NULL, HOTKEY_ID_SWITCH_BASE + i);
		UnregisterHotKey(NULL, HOTKEY_ID_MOVE_BASE + i);
	}
	UnregisterHotKey(NULL, HOTKEY_ID_EXIT);
	UnregisterHotKey(NULL, HOTKEY_ID_TOGGLE);
	UnregisterHotKey(NULL, HOTKEY_ID_PREV);
	UnregisterHotKey(NULL, HOTKEY_ID_NEXT);
}

static unsigned virgo_register_all_hotkeys(Virgo *v)
{
	unsigned i;
	for (i = 0; i < NUM_DESKTOPS; i++) {
		if (v->hotkeys_switch[i].vk) {
			if (!register_hotkey(HOTKEY_ID_SWITCH_BASE + i,
					v->hotkeys_switch[i].modifiers | MOD_NOREPEAT,
					v->hotkeys_switch[i].vk)) {
				virgo_unregister_all_hotkeys(v);
				return 0;
			}
		}
		if (v->hotkeys_move[i].vk) {
			if (!register_hotkey(HOTKEY_ID_MOVE_BASE + i,
					v->hotkeys_move[i].modifiers | MOD_NOREPEAT,
					v->hotkeys_move[i].vk)) {
				virgo_unregister_all_hotkeys(v);
				return 0;
			}
		}
	}
	if (!register_hotkey(HOTKEY_ID_EXIT,
			MOD_ALT | MOD_CONTROL | MOD_SHIFT | MOD_NOREPEAT, 'Q')) {
		virgo_unregister_all_hotkeys(v);
		return 0;
	}
	if (!register_hotkey(HOTKEY_ID_TOGGLE,
			MOD_ALT | MOD_CONTROL | MOD_SHIFT | MOD_NOREPEAT, 'S')) {
		virgo_unregister_all_hotkeys(v);
		return 0;
	}
	if (v->hotkey_prev.vk) {
		if (!register_hotkey(HOTKEY_ID_PREV,
				v->hotkey_prev.modifiers | MOD_NOREPEAT,
				v->hotkey_prev.vk)) {
			virgo_unregister_all_hotkeys(v);
			return 0;
		}
	}
	if (v->hotkey_next.vk) {
		if (!register_hotkey(HOTKEY_ID_NEXT,
				v->hotkey_next.modifiers | MOD_NOREPEAT,
				v->hotkey_next.vk)) {
			virgo_unregister_all_hotkeys(v);
			return 0;
		}
	}
	return 1;
}

static void windows_show(Windows *wins)
{
	unsigned i;
	for (i = 0; i < wins->count; i++) {
		HWND hwnd = wins->windows[i];
		virgo_set_window_cloak(hwnd, 0);
		virgo_set_window_state(hwnd, 0);
		ShowWindow(hwnd, SW_SHOW);
	}
}

static void windows_add(Windows *wins, HWND hwnd)
{
	if (wins->count >= sb_count(wins->windows)) {
		sb_push(wins->windows, hwnd);
	} else {
		wins->windows[wins->count] = hwnd;
	}
	wins->count++;
}

static void windows_del(Windows *wins, HWND hwnd)
{
	unsigned i, e;
	for (i = 0; i < wins->count; i++) {
		if (wins->windows[i] != hwnd) {
			continue;
		}
		if (i != wins->count - 1) {
			for (e = i; e < wins->count - 1; e++) {
				wins->windows[e] = wins->windows[e + 1];
			}
		}
		wins->count--;
		break;
	}
}

static unsigned is_valid_window(HWND hwnd)
{
	WINDOWINFO wi;
	wi.cbSize = sizeof(wi);
	GetWindowInfo(hwnd, &wi);
	return (wi.dwStyle & WS_VISIBLE) && !(wi.dwExStyle & WS_EX_TOOLWINDOW);
}

static unsigned register_hotkey(unsigned id, unsigned mod, unsigned vk)
{
	if (!RegisterHotKey(NULL, id, mod, vk)) {
		return 0;
	}
	return 1;
}

static BOOL enum_func(HWND hwnd, LPARAM lParam)
{
	Virgo *v;
	MonitorState *state;
	unsigned monitor_idx;
	v = (Virgo *)lParam;
	if (!is_valid_window(hwnd)) {
		return 1;
	}
	if (virgo_contains_window(v, hwnd)) {
		return 1;
	}
	monitor_idx = virgo_monitor_from_hwnd(v, hwnd);
	state = &v->monitors[monitor_idx];
	windows_add(&state->desktops[state->current], hwnd);
	virgo_set_window_visibility(v, hwnd, 1);
	return 1;
}

static void virgo_update(Virgo *v)
{
	unsigned m, d;
	MonitorState *state;
	Windows *desk;
	unsigned idx;
	HWND hwnd;
	for (m = 0; m < v->monitor_count; m++) {
		state = &v->monitors[m];
		for (d = 0; d < NUM_DESKTOPS; d++) {
			desk = &state->desktops[d];
			idx = 0;
			while (idx < desk->count) {
				unsigned new_idx;
				hwnd = desk->windows[idx];
				if (!GetWindowThreadProcessId(hwnd, NULL)) {
					windows_del(desk, hwnd);
					continue;
				}
				if (d == state->current && !IsWindowVisible(hwnd)) {
					windows_del(desk, hwnd);
					continue;
				}
				new_idx = virgo_monitor_from_hwnd(v, hwnd);
				if (new_idx != m) {
					windows_del(desk, hwnd);
					windows_add(&v->monitors[new_idx]
							.desktops[v->monitors[new_idx].current],
							hwnd);
					virgo_set_window_visibility(v, hwnd, 1);
					continue;
				}
				idx++;
			}
		}
	}
	EnumWindows((WNDENUMPROC)&enum_func, (LPARAM)v);
}

static void virgo_switch_monitor_to_desk(Virgo *v, unsigned monitor_idx,
		unsigned desk, HWND preferred)
{
	MonitorState *state;
	unsigned old;
	unsigned prev_suppress;
	if (monitor_idx >= v->monitor_count || desk >= NUM_DESKTOPS) {
		return;
	}
	state = &v->monitors[monitor_idx];
	if (state->current == desk) {
		return;
	}
	virgo_update(v);
	prev_suppress = v->suppress_foreground;
	v->suppress_foreground = 1;
	old = state->current;
	if (!prev_suppress) {
		HWND fg = GetForegroundWindow();
		if (fg && is_valid_window(fg)) {
			unsigned fg_monitor;
			unsigned fg_desk;
			if (virgo_find_window(v, fg, &fg_monitor, &fg_desk) && fg_monitor == monitor_idx &&
			    fg_desk == old) {
				state->last_active[old] = fg;
			}
		}
	}
	virgo_apply_desktop_visibility(v, state, desk, 1, preferred);
	state->current = desk;
	virgo_apply_desktop_visibility(v, state, old, 0, NULL);
	v->suppress_foreground = prev_suppress;
	if (!prev_suppress) {
		/*
		 * Give the foreground hook a 250 ms grace period so it ignores the
		 * deactivation noise we generate while hiding the previous desktop's
		 * windows. The hook will only resume reacting to foreground changes after
		 * GetTickCount() surpasses this timestamp, which keeps us from instantly
		 * switching back because of the transient EVENT_SYSTEM_FOREGROUND events.
		 * 250 ms is long enough to cover the hide/show cycle yet short enough to
		 * stay unnoticed by the user.
		 */
		state->suppress_foreground_until = GetTickCount() + 250;
	}
	if (!preferred) {
		preferred = state->last_active[desk];
	}
	if (preferred) {
		unsigned target_monitor;
		unsigned target_desk;
		if (IsIconic(preferred)) {
			preferred = NULL;
		}
		if (preferred &&
		    (!virgo_find_window(v, preferred, &target_monitor, &target_desk) ||
		     target_monitor != monitor_idx || target_desk != desk)) {
			preferred = NULL;
		}
	}
	if (!preferred) {
		Windows *active_desk = &state->desktops[desk];
		unsigned i;
		for (i = 0; i < active_desk->count; i++) {
			HWND candidate = active_desk->windows[i];
			if (!IsWindow(candidate)) {
				continue;
			}
			if (IsIconic(candidate)) {
				continue;
			}
			if (!IsWindowVisible(candidate)) {
				continue;
			}
			preferred = candidate;
			break;
		}
	}
	if (preferred) {
		virgo_activate_window(preferred);
		state->last_active[desk] = preferred;
	}
	trayicon_refresh(v);
}

static void virgo_apply_all_visibility(Virgo *v)
{
	unsigned m, d;
	for (m = 0; m < v->monitor_count; m++) {
		MonitorState *state = &v->monitors[m];
		for (d = 0; d < NUM_DESKTOPS; d++) {
			virgo_apply_desktop_visibility(v, state, d, d == state->current,
				state->last_active[d]);
		}
	}
}

static void virgo_set_show_all_taskbar(Virgo *v, unsigned enabled)
{
	if (v->show_all_taskbar == enabled) {
		return;
	}
	v->show_all_taskbar = enabled;
	virgo_update_foreground_hook(v);
	virgo_apply_all_visibility(v);
	virgo_save_config(v);
}

static void virgo_sync_pending_from_active(Virgo *v)
{
	unsigned i;
	for (i = 0; i < NUM_DESKTOPS; i++) {
		v->pending_switch[i] = v->hotkeys_switch[i];
		v->pending_move[i] = v->hotkeys_move[i];
	}
	v->pending_prev = v->hotkey_prev;
	v->pending_next = v->hotkey_next;
	v->pending_dirty = 0;
}

static void virgo_append_text(char *buffer, unsigned buffer_size,
					   const char *text)
{
	unsigned i;
	unsigned len;
	if (!buffer || !text || buffer_size == 0) {
		return;
	}
	len = 0;
	while (buffer[len] && len + 1 < buffer_size) {
		len++;
	}
	for (i = 0; text[i] && len + 1 < buffer_size; i++) {
		buffer[len++] = text[i];
	}
	buffer[len] = 0;
}

static void virgo_append_number(char *buffer, unsigned buffer_size,
					     unsigned value)
{
	char digits[10];
	unsigned count = 0;
	unsigned i;
	if (value == 0) {
		virgo_append_text(buffer, buffer_size, "0");
		return;
	}
	while (value && count < sizeof(digits)) {
		digits[count++] = (char)('0' + (value % 10));
		value /= 10;
	}
	for (i = 0; i < count; i++) {
		char c[2];
		c[0] = digits[count - 1 - i];
		c[1] = 0;
		virgo_append_text(buffer, buffer_size, c);
	}
}

static unsigned virgo_parse_uint(const char *text, const char **endptr,
				     unsigned *ok)
{
	unsigned value = 0;
	unsigned has_digit = 0;
	const char *ptr = text;
	while (*ptr >= '0' && *ptr <= '9') {
		has_digit = 1;
		value = value * 10 + (unsigned)(*ptr - '0');
		ptr++;
	}
	if (endptr) {
		*endptr = ptr;
	}
	if (ok) {
		*ok = has_digit;
	}
	return value;
}

static unsigned virgo_string_equals(const char *a, const char *b)
{
	unsigned i = 0;
	while (a[i] && b[i]) {
		if (a[i] != b[i]) {
			return 0;
		}
		i++;
	}
	return a[i] == 0 && b[i] == 0;
}

static unsigned virgo_string_starts_with(const char *text, const char *prefix)
{
	unsigned i = 0;
	while (prefix[i]) {
		if (text[i] != prefix[i]) {
			return 0;
		}
		i++;
	}
	return 1;
}

static void virgo_init_config_path(Virgo *v)
{
	char path[MAX_PATH];
	DWORD len;
	char *last_sep = NULL;
	if (!v) {
		return;
	}
	memclr(v->config_path, sizeof(v->config_path));
	len = GetModuleFileNameA(NULL, path, sizeof(path));
	if (!len || len >= sizeof(path)) {
		virgo_append_text(v->config_path, sizeof(v->config_path), "virgo.cfg");
		return;
	}
	path[len] = 0;
	if (len) {
		unsigned i;
		for (i = len; i > 0; i--) {
			if (path[i - 1] == '\\' || path[i - 1] == '/') {
				last_sep = &path[i - 1];
				break;
			}
		}
	}
	if (last_sep) {
		last_sep[1] = 0;
	} else {
		path[0] = 0;
	}
	virgo_append_text(v->config_path, sizeof(v->config_path), path);
	virgo_append_text(v->config_path, sizeof(v->config_path), "virgo.cfg");
}

static void virgo_process_config_line(Virgo *v, const char *line)
{
	char key[32];
	const char *value;
	unsigned key_len;
	unsigned i;
	if (!v || !line) {
		return;
	}
	while (*line == ' ' || *line == '\t') {
		line++;
	}
	if (*line == 0 || *line == ';' || *line == '#') {
		return;
	}
	value = line;
	while (*value && *value != '=') {
		value++;
	}
	if (*value != '=') {
		return;
	}
	key_len = (unsigned)(value - line);
	while (key_len && (line[key_len - 1] == ' ' || line[key_len - 1] == '\t')) {
		key_len--;
	}
	value++;
	while (*value == ' ' || *value == '\t') {
		value++;
	}
	if (!key_len) {
		return;
	}
	if (key_len >= sizeof(key)) {
		key_len = sizeof(key) - 1;
	}
	for (i = 0; i < key_len; i++) {
		key[i] = line[i];
	}
	key[key_len] = 0;
	if (!value[0]) {
		return;
	}
	if (virgo_string_starts_with(key, "switch")) {
		unsigned idx;
		unsigned modifiers;
		unsigned vk;
		const char *ptr;
		unsigned ok;
		idx = virgo_parse_uint(key + 6, &ptr, &ok);
		if (!ok || *ptr || idx >= NUM_DESKTOPS) {
			return;
		}
		ptr = value;
		modifiers = virgo_parse_uint(ptr, &ptr, &ok);
		if (!ok) {
			return;
		}
		while (*ptr == ' ' || *ptr == '\t') {
			ptr++;
		}
		if (*ptr != ',') {
			return;
		}
		ptr++;
		while (*ptr == ' ' || *ptr == '\t') {
			ptr++;
		}
		vk = virgo_parse_uint(ptr, &ptr, &ok);
		if (!ok) {
			return;
		}
		while (*ptr == ' ' || *ptr == '\t') {
			ptr++;
		}
		if (*ptr) {
			return;
		}
		modifiers &= (MOD_ALT | MOD_CONTROL | MOD_SHIFT | MOD_WIN);
		v->hotkeys_switch[idx].modifiers = modifiers;
		v->hotkeys_switch[idx].vk = vk;
		return;
	}
	if (virgo_string_starts_with(key, "move")) {
		unsigned idx;
		unsigned modifiers;
		unsigned vk;
		const char *ptr;
		unsigned ok;
		idx = virgo_parse_uint(key + 4, &ptr, &ok);
		if (!ok || *ptr || idx >= NUM_DESKTOPS) {
			return;
		}
		ptr = value;
		modifiers = virgo_parse_uint(ptr, &ptr, &ok);
		if (!ok) {
			return;
		}
		while (*ptr == ' ' || *ptr == '\t') {
			ptr++;
		}
		if (*ptr != ',') {
			return;
		}
		ptr++;
		while (*ptr == ' ' || *ptr == '\t') {
			ptr++;
		}
		vk = virgo_parse_uint(ptr, &ptr, &ok);
		if (!ok) {
			return;
		}
		while (*ptr == ' ' || *ptr == '\t') {
			ptr++;
		}
		if (*ptr) {
			return;
		}
		modifiers &= (MOD_ALT | MOD_CONTROL | MOD_SHIFT | MOD_WIN);
		v->hotkeys_move[idx].modifiers = modifiers;
		v->hotkeys_move[idx].vk = vk;
		return;
	}
	if (virgo_string_equals(key, "prev")) {
		const char *ptr;
		unsigned modifiers;
		unsigned vk;
		unsigned ok;
		ptr = value;
		modifiers = virgo_parse_uint(ptr, &ptr, &ok);
		if (!ok) {
			return;
		}
		while (*ptr == ' ' || *ptr == '\t') {
			ptr++;
		}
		if (*ptr != ',') {
			return;
		}
		ptr++;
		while (*ptr == ' ' || *ptr == '\t') {
			ptr++;
		}
		vk = virgo_parse_uint(ptr, &ptr, &ok);
		if (!ok) {
			return;
		}
		while (*ptr == ' ' || *ptr == '\t') {
			ptr++;
		}
		if (*ptr) {
			return;
		}
		modifiers &= (MOD_ALT | MOD_CONTROL | MOD_SHIFT | MOD_WIN);
		v->hotkey_prev.modifiers = modifiers;
		v->hotkey_prev.vk = vk;
		return;
	}
	if (virgo_string_equals(key, "next")) {
		const char *ptr;
		unsigned modifiers;
		unsigned vk;
		unsigned ok;
		ptr = value;
		modifiers = virgo_parse_uint(ptr, &ptr, &ok);
		if (!ok) {
			return;
		}
		while (*ptr == ' ' || *ptr == '\t') {
			ptr++;
		}
		if (*ptr != ',') {
			return;
		}
		ptr++;
		while (*ptr == ' ' || *ptr == '\t') {
			ptr++;
		}
		vk = virgo_parse_uint(ptr, &ptr, &ok);
		if (!ok) {
			return;
		}
		while (*ptr == ' ' || *ptr == '\t') {
			ptr++;
		}
		if (*ptr) {
			return;
		}
		modifiers &= (MOD_ALT | MOD_CONTROL | MOD_SHIFT | MOD_WIN);
		v->hotkey_next.modifiers = modifiers;
		v->hotkey_next.vk = vk;
		return;
	}
	if (virgo_string_equals(key, "show_all_taskbar")) {
		const char *ptr;
		unsigned ok;
		unsigned val;
		ptr = value;
		val = virgo_parse_uint(ptr, &ptr, &ok);
		if (!ok) {
			return;
		}
		while (*ptr == ' ' || *ptr == '\t') {
			ptr++;
		}
		if (*ptr) {
			return;
		}
		v->show_all_taskbar = val ? 1 : 0;
	}
}

static void virgo_load_config(Virgo *v)
{
	HANDLE file;
	DWORD file_size;
	DWORD bytes_read;
	char *buffer;
	char *line;
	if (!v || !v->config_path[0]) {
		return;
	}
	file = CreateFileA(v->config_path, GENERIC_READ, FILE_SHARE_READ, NULL,
			OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (file == INVALID_HANDLE_VALUE) {
		return;
	}
	file_size = GetFileSize(file, NULL);
	if (file_size == INVALID_FILE_SIZE) {
		CloseHandle(file);
		return;
	}
	if (file_size > 64 * 1024) {
		file_size = 64 * 1024;
	}
	buffer = (char *)HeapAlloc(GetProcessHeap(), 0, file_size + 1);
	if (!buffer) {
		CloseHandle(file);
		return;
	}
	if (!ReadFile(file, buffer, file_size, &bytes_read, NULL)) {
		HeapFree(GetProcessHeap(), 0, buffer);
		CloseHandle(file);
		return;
	}
	buffer[bytes_read] = 0;
	CloseHandle(file);
	line = buffer;
	while (*line) {
		char *end = line;
		while (*end && *end != '\r' && *end != '\n') {
			end++;
		}
		if (*end) {
			char saved = *end;
			*end = 0;
			virgo_process_config_line(v, line);
			*end = saved;
			if (saved == '\r' && end[1] == '\n') {
				line = end + 2;
			} else {
				line = end + 1;
			}
		} else {
			virgo_process_config_line(v, line);
			break;
		}
	}
	HeapFree(GetProcessHeap(), 0, buffer);
}

static void virgo_save_config(Virgo *v)
{
	HANDLE file;
	unsigned i;
	char line[128];
	DWORD written;
	if (!v || !v->config_path[0]) {
		return;
	}
	file = CreateFileA(v->config_path, GENERIC_WRITE, FILE_SHARE_READ, NULL,
			CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	if (file == INVALID_HANDLE_VALUE) {
		return;
	}
	lstrcpynA(line, "; virgo configuration\r\n", sizeof(line));
	WriteFile(file, line, (DWORD)lstrlenA(line), &written, NULL);
	wsprintfA(line, "show_all_taskbar=%u\r\n", v->show_all_taskbar ? 1 : 0);
	WriteFile(file, line, (DWORD)lstrlenA(line), &written, NULL);
	wsprintfA(line, "prev=%u,%u\r\n", v->hotkey_prev.modifiers,
		v->hotkey_prev.vk);
	WriteFile(file, line, (DWORD)lstrlenA(line), &written, NULL);
	wsprintfA(line, "next=%u,%u\r\n", v->hotkey_next.modifiers,
		v->hotkey_next.vk);
	WriteFile(file, line, (DWORD)lstrlenA(line), &written, NULL);
	for (i = 0; i < NUM_DESKTOPS; i++) {
		wsprintfA(line, "switch%u=%u,%u\r\n", i,
			v->hotkeys_switch[i].modifiers, v->hotkeys_switch[i].vk);
		WriteFile(file, line, (DWORD)lstrlenA(line), &written, NULL);
		wsprintfA(line, "move%u=%u,%u\r\n", i,
			v->hotkeys_move[i].modifiers, v->hotkeys_move[i].vk);
		WriteFile(file, line, (DWORD)lstrlenA(line), &written, NULL);
	}
	CloseHandle(file);
}

static void virgo_hotkey_to_string(const Hotkey *hk, char *buffer,
						unsigned buffer_size)
{
	unsigned vk;
	if (!buffer || buffer_size == 0) {
		return;
	}
	buffer[0] = 0;
	if (!hk) {
		return;
	}
	if (hk->modifiers & MOD_CONTROL) {
		virgo_append_text(buffer, buffer_size, "Ctrl+");
	}
	if (hk->modifiers & MOD_ALT) {
		virgo_append_text(buffer, buffer_size, "Alt+");
	}
	if (hk->modifiers & MOD_SHIFT) {
		virgo_append_text(buffer, buffer_size, "Shift+");
	}
	if (hk->modifiers & MOD_WIN) {
		virgo_append_text(buffer, buffer_size, "Win+");
	}
	vk = hk->vk;
	if (vk >= 'A' && vk <= 'Z') {
		char c[2];
		c[0] = (char)vk;
		c[1] = 0;
		virgo_append_text(buffer, buffer_size, c);
		return;
	}
	if (vk >= '0' && vk <= '9') {
		char c[2];
		c[0] = (char)vk;
		c[1] = 0;
		virgo_append_text(buffer, buffer_size, c);
		return;
	}
	if (vk >= VK_F1 && vk <= VK_F24) {
		virgo_append_text(buffer, buffer_size, "F");
		virgo_append_number(buffer, buffer_size, vk - VK_F1 + 1);
		return;
	}
	if (vk == VK_TAB) {
		virgo_append_text(buffer, buffer_size, "Tab");
		return;
	}
	if (vk == VK_ESCAPE) {
		virgo_append_text(buffer, buffer_size, "Esc");
		return;
	}
	if (vk == VK_SPACE) {
		virgo_append_text(buffer, buffer_size, "Space");
		return;
	}
	if (vk == VK_RETURN) {
		virgo_append_text(buffer, buffer_size, "Enter");
		return;
	}
	if (vk == VK_BACK) {
		virgo_append_text(buffer, buffer_size, "Backspace");
		return;
	}
	if (vk == VK_DELETE) {
		virgo_append_text(buffer, buffer_size, "Delete");
		return;
	}
	if (vk == VK_HOME) {
		virgo_append_text(buffer, buffer_size, "Home");
		return;
	}
	if (vk == VK_END) {
		virgo_append_text(buffer, buffer_size, "End");
		return;
	}
	if (vk == VK_PRIOR) {
		virgo_append_text(buffer, buffer_size, "PageUp");
		return;
	}
	if (vk == VK_NEXT) {
		virgo_append_text(buffer, buffer_size, "PageDown");
		return;
	}
	if (vk == VK_LEFT) {
		virgo_append_text(buffer, buffer_size, "Left");
		return;
	}
	if (vk == VK_RIGHT) {
		virgo_append_text(buffer, buffer_size, "Right");
		return;
	}
	if (vk == VK_UP) {
		virgo_append_text(buffer, buffer_size, "Up");
		return;
	}
	if (vk == VK_DOWN) {
		virgo_append_text(buffer, buffer_size, "Down");
		return;
	}
	virgo_append_text(buffer, buffer_size, "VK");
	virgo_append_number(buffer, buffer_size, vk);
}

static UINT virgo_get_modifier_state(void)
{
	UINT mod = 0;
	if (GetAsyncKeyState(VK_CONTROL) & 0x8000) {
		mod |= MOD_CONTROL;
	}
	if (GetAsyncKeyState(VK_MENU) & 0x8000) {
		mod |= MOD_ALT;
	}
	if (GetAsyncKeyState(VK_SHIFT) & 0x8000) {
		mod |= MOD_SHIFT;
	}
	if ((GetAsyncKeyState(VK_LWIN) & 0x8000) ||
		(GetAsyncKeyState(VK_RWIN) & 0x8000)) {
		mod |= MOD_WIN;
	}
	return mod;
}

static void virgo_update_config_button_text(Virgo *v, unsigned type,
						unsigned index)
{
	char text[64];
	Hotkey *hk = NULL;
	HWND button = NULL;
	if (!v) {
		return;
	}
	switch (type) {
	case CAPTURE_TYPE_SWITCH:
		if (index >= NUM_DESKTOPS) {
			return;
		}
		hk = &v->pending_switch[index];
		button = v->switch_buttons[index];
		break;
	case CAPTURE_TYPE_MOVE:
		if (index >= NUM_DESKTOPS) {
			return;
		}
		hk = &v->pending_move[index];
		button = v->move_buttons[index];
		break;
	case CAPTURE_TYPE_PREV:
		hk = &v->pending_prev;
		button = v->button_prev;
		break;
	case CAPTURE_TYPE_NEXT:
		hk = &v->pending_next;
		button = v->button_next;
		break;
	default:
		return;
	}
	virgo_hotkey_to_string(hk, text, sizeof(text));
	if (!text[0]) {
		virgo_append_text(text, sizeof(text), "Unassigned");
	}
	if (button) {
		SetWindowTextA(button, text);
	}
}

static void virgo_update_all_config_text(Virgo *v)
{
	unsigned i;
	for (i = 0; i < NUM_DESKTOPS; i++) {
		virgo_update_config_button_text(v, CAPTURE_TYPE_SWITCH, i);
		virgo_update_config_button_text(v, CAPTURE_TYPE_MOVE, i);
	}
	virgo_update_config_button_text(v, CAPTURE_TYPE_PREV, 0);
	virgo_update_config_button_text(v, CAPTURE_TYPE_NEXT, 0);
}

static void virgo_cancel_capture(Virgo *v)
{
	unsigned type;
	unsigned index;
	if (v->capturing_type == CAPTURE_TYPE_NONE) {
		return;
	}
	type = v->capturing_type;
	index = v->capturing_index;
	v->capturing_type = CAPTURE_TYPE_NONE;
	v->capturing_index = 0;
	virgo_update_config_button_text(v, type, index);
}

static void virgo_begin_capture(Virgo *v, unsigned type, unsigned index)
{
	HWND button;
	virgo_cancel_capture(v);
	if ((type == CAPTURE_TYPE_SWITCH || type == CAPTURE_TYPE_MOVE) &&
		index >= NUM_DESKTOPS) {
		return;
	}
	v->capturing_type = type;
	v->capturing_index = index;
	v->pending_dirty = 1;
	if (type == CAPTURE_TYPE_SWITCH) {
		button = v->switch_buttons[index];
	} else if (type == CAPTURE_TYPE_MOVE) {
		button = v->move_buttons[index];
	} else if (type == CAPTURE_TYPE_PREV) {
		button = v->button_prev;
	} else {
		button = v->button_next;
	}
	if (button) {
		SetWindowTextA(button, "Press keys...");
	}
	if (v->config_window) {
		SetFocus(v->config_window);
	}
}

static void virgo_apply_configured_hotkeys(Virgo *v)
{
	unsigned i;
	Hotkey old_switch[NUM_DESKTOPS];
	Hotkey old_move[NUM_DESKTOPS];
	Hotkey old_prev;
	Hotkey old_next;
	for (i = 0; i < NUM_DESKTOPS; i++) {
		old_switch[i] = v->hotkeys_switch[i];
		old_move[i] = v->hotkeys_move[i];
		v->hotkeys_switch[i] = v->pending_switch[i];
		v->hotkeys_move[i] = v->pending_move[i];
	}
	old_prev = v->hotkey_prev;
	old_next = v->hotkey_next;
	v->hotkey_prev = v->pending_prev;
	v->hotkey_next = v->pending_next;
	v->pending_dirty = 0;
	if (v->handle_hotkeys) {
		virgo_unregister_all_hotkeys(v);
		if (!virgo_register_all_hotkeys(v)) {
			MessageBox(NULL,
				   "Hotkey registration failed. Please choose different hotkeys.",
				   "virgo", MB_ICONEXCLAMATION);
			for (i = 0; i < NUM_DESKTOPS; i++) {
				v->hotkeys_switch[i] = old_switch[i];
				v->hotkeys_move[i] = old_move[i];
			}
			v->hotkey_prev = old_prev;
			v->hotkey_next = old_next;
			virgo_register_all_hotkeys(v);
			virgo_sync_pending_from_active(v);
		}
	}
	virgo_update_all_config_text(v);
	virgo_save_config(v);
}

static void virgo_show_config_window(Virgo *v)
{
	WNDCLASSA wc;
	static ATOM config_atom;
	HWND hwnd;
	if (v->config_window) {
		ShowWindow(v->config_window, SW_SHOWNORMAL);
		SetForegroundWindow(v->config_window);
		return;
	}
	virgo_sync_pending_from_active(v);
	if (!config_atom) {
		memclr(&wc, sizeof(wc));
		wc.lpfnWndProc = &config_wndproc;
		wc.hInstance = GetModuleHandle(NULL);
		wc.lpszClassName = "VirgoConfigWindow";
		config_atom = RegisterClassA(&wc);
		if (!config_atom) {
			MessageBox(NULL, "could not register config window", "error",
				   MB_ICONEXCLAMATION);
			return;
		}
	}
	hwnd = CreateWindowExA(WS_EX_TOOLWINDOW, "VirgoConfigWindow",
		     "Virgo Hotkeys", WS_OVERLAPPED | WS_CAPTION |
			 WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX,
			 CW_USEDEFAULT, CW_USEDEFAULT, 420, 420, NULL, NULL,
			 GetModuleHandle(NULL), v);
	if (!hwnd) {
		MessageBox(NULL, "could not create config window", "error",
			   MB_ICONEXCLAMATION);
		return;
	}
	v->config_window = hwnd;
	ShowWindow(hwnd, SW_SHOW);
	UpdateWindow(hwnd);
}

static void virgo_update_foreground_hook(Virgo *v)
{
	if (!v) {
		return;
	}
	if (v->show_all_taskbar) {
		if (!v->foreground_hook) {
			v->foreground_hook =
				SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND,
						NULL, virgo_foreground_proc, 0, 0,
						WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS);
		}
	} else if (v->foreground_hook) {
		UnhookWinEvent(v->foreground_hook);
		v->foreground_hook = NULL;
	}
}

static void CALLBACK virgo_foreground_proc(HWINEVENTHOOK hook, DWORD event,
						HWND hwnd, LONG idObject, LONG idChild,
						DWORD idEventThread, DWORD dwmsEventTime)
{
	Virgo *v;
	unsigned monitor_idx;
	unsigned desk_idx;
	MonitorState *state;
	(void)hook;
	(void)event;
	(void)idEventThread;
	(void)dwmsEventTime;
	if (idObject != OBJID_WINDOW || idChild != 0) {
		return;
	}
	v = g_virgo;
	if (!v || !v->show_all_taskbar || v->suppress_foreground) {
		return;
	}
	if (!is_valid_window(hwnd)) {
		return;
	}
	if (!virgo_find_window(v, hwnd, &monitor_idx, &desk_idx)) {
		return;
	}
	state = &v->monitors[monitor_idx];
	if (state->suppress_foreground_until) {
		DWORD now = GetTickCount();
		if ((LONG)(now - state->suppress_foreground_until) < 0) {
			return;
		}
		state->suppress_foreground_until = 0;
	}
	if (v->monitors[monitor_idx].current == desk_idx) {
		return;
	}
	v->suppress_foreground = 1;
	virgo_switch_monitor_to_desk(v, monitor_idx, desk_idx, hwnd);
	v->suppress_foreground = 0;
}

static LRESULT CALLBACK config_wndproc(HWND hwnd, UINT msg, WPARAM wParam,
						LPARAM lParam)
{
	Virgo *v;
	v = (Virgo *)GetWindowLongPtr(hwnd, GWLP_USERDATA);
	switch (msg) {
	case WM_CREATE: {
		unsigned i;
		int y;
		char text[64];
		CREATESTRUCT *cs;
		cs = (CREATESTRUCT *)lParam;
		v = (Virgo *)cs->lpCreateParams;
		SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)v);
		if (!v) {
			return 0;
		}
		v->config_window = hwnd;
		virgo_cancel_capture(v);
		CreateWindowExA(0, "STATIC",
			       "Click a button and press the new hotkey combination.",
			       WS_CHILD | WS_VISIBLE, 10, 10, 380, 18, hwnd, NULL,
			       GetModuleHandle(NULL), NULL);
		CreateWindowExA(0, "STATIC",
			       "Switch desktop", WS_CHILD | WS_VISIBLE, 120, 40,
			       120, 18, hwnd, NULL, GetModuleHandle(NULL), NULL);
		CreateWindowExA(0, "STATIC",
			       "Move window", WS_CHILD | WS_VISIBLE, 260, 40,
			       120, 18, hwnd, NULL, GetModuleHandle(NULL), NULL);
		for (i = 0; i < NUM_DESKTOPS; i++) {
			y = 70 + (int)i * 40;
			text[0] = 0;
			virgo_append_text(text, sizeof(text), "Desktop ");
			virgo_append_number(text, sizeof(text), i + 1);
			CreateWindowExA(0, "STATIC", text, WS_CHILD | WS_VISIBLE,
				       10, y + 6, 90, 18, hwnd, NULL,
				       GetModuleHandle(NULL), NULL);
			v->switch_buttons[i] = CreateWindowExA(
				0, "BUTTON", "", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
				120, y, 130, 26, hwnd,
				(HMENU)(UINT_PTR)(IDC_SWITCH_BASE + i),
				GetModuleHandle(NULL), NULL);
			v->move_buttons[i] = CreateWindowExA(
				0, "BUTTON", "", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
				260, y, 130, 26, hwnd,
				(HMENU)(UINT_PTR)(IDC_MOVE_BASE + i),
				GetModuleHandle(NULL), NULL);
		}
		y = 70 + (int)NUM_DESKTOPS * 40;
		CreateWindowExA(0, "STATIC", "Previous desktop", WS_CHILD | WS_VISIBLE,
		       10, y + 6, 130, 18, hwnd, NULL, GetModuleHandle(NULL), NULL);
		v->button_prev = CreateWindowExA(
			0, "BUTTON", "", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
			120, y, 130, 26, hwnd, (HMENU)(UINT_PTR)IDC_PREV,
			GetModuleHandle(NULL), NULL);
		y += 40;
		CreateWindowExA(0, "STATIC", "Next desktop", WS_CHILD | WS_VISIBLE,
		       10, y + 6, 130, 18, hwnd, NULL, GetModuleHandle(NULL), NULL);
		v->button_next = CreateWindowExA(
			0, "BUTTON", "", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
			120, y, 130, 26, hwnd, (HMENU)(UINT_PTR)IDC_NEXT,
			GetModuleHandle(NULL), NULL);
		y += 40;
		CreateWindowExA(0, "BUTTON", "Apply",
			       WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
		       180, y, 90, 28, hwnd,
			       (HMENU)(UINT_PTR)IDC_APPLY, GetModuleHandle(NULL), NULL);
		CreateWindowExA(0, "BUTTON", "Close",
			       WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
		       280, y, 90, 28, hwnd,
			       (HMENU)(UINT_PTR)IDC_CLOSE, GetModuleHandle(NULL), NULL);
		{
			RECT client;
			int content_bottom = y + 28 + 10;
			if (GetClientRect(hwnd, &client) && content_bottom > client.bottom) {
				RECT desired;
				DWORD style;
				DWORD exstyle;
				desired.left = 0;
				desired.top = 0;
				desired.right = client.right;
				desired.bottom = content_bottom;
				style = (DWORD)GetWindowLongPtr(hwnd, GWL_STYLE);
				exstyle = (DWORD)GetWindowLongPtr(hwnd, GWL_EXSTYLE);
				AdjustWindowRectEx(&desired, style, FALSE, exstyle);
				SetWindowPos(hwnd, NULL, 0, 0,
					desired.right - desired.left,
					desired.bottom - desired.top,
					SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
			}
		}
		virgo_update_all_config_text(v);
		return 0;
	}
	case WM_COMMAND: {
		unsigned id;
		if (!v) {
			break;
		}
		if (HIWORD(wParam) != BN_CLICKED) {
			break;
		}
		id = LOWORD(wParam);
		if (id >= IDC_SWITCH_BASE && id < IDC_SWITCH_BASE + NUM_DESKTOPS) {
			virgo_begin_capture(v, CAPTURE_TYPE_SWITCH, id - IDC_SWITCH_BASE);
			return 0;
		}
		if (id >= IDC_MOVE_BASE && id < IDC_MOVE_BASE + NUM_DESKTOPS) {
			virgo_begin_capture(v, CAPTURE_TYPE_MOVE, id - IDC_MOVE_BASE);
			return 0;
		}
		if (id == IDC_PREV) {
			virgo_begin_capture(v, CAPTURE_TYPE_PREV, 0);
			return 0;
		}
		if (id == IDC_NEXT) {
			virgo_begin_capture(v, CAPTURE_TYPE_NEXT, 0);
			return 0;
		}
		if (id == IDC_APPLY) {
			virgo_cancel_capture(v);
			virgo_apply_configured_hotkeys(v);
			return 0;
		}
		if (id == IDC_CLOSE) {
			DestroyWindow(hwnd);
			return 0;
		}
		break;
	}
	case WM_KEYDOWN:
	case WM_SYSKEYDOWN: {
		UINT vk;
		UINT modifiers;
		Hotkey *target;
		unsigned type;
		unsigned index;
		if (!v || v->capturing_type == CAPTURE_TYPE_NONE) {
			break;
		}
		vk = (UINT)wParam;
		modifiers = virgo_get_modifier_state();
		if (vk == VK_ESCAPE && modifiers == 0) {
			virgo_cancel_capture(v);
			return 0;
		}
		if (vk == VK_CONTROL || vk == VK_SHIFT || vk == VK_MENU ||
			vk == VK_LWIN || vk == VK_RWIN) {
			return 0;
		}
		if (!(modifiers & (MOD_ALT | MOD_CONTROL | MOD_SHIFT | MOD_WIN))) {
			MessageBox(hwnd,
				   "Please include at least one modifier (Ctrl, Alt, Shift, Win).",
				   "virgo", MB_ICONEXCLAMATION);
			return 0;
		}
		switch (v->capturing_type) {
		case CAPTURE_TYPE_SWITCH:
			target = &v->pending_switch[v->capturing_index];
			break;
		case CAPTURE_TYPE_MOVE:
			target = &v->pending_move[v->capturing_index];
			break;
		case CAPTURE_TYPE_PREV:
			target = &v->pending_prev;
			break;
		case CAPTURE_TYPE_NEXT:
			target = &v->pending_next;
			break;
		default:
			return 0;
		}
		target->modifiers = modifiers;
		target->vk = vk;
		v->pending_dirty = 1;
		type = v->capturing_type;
		index = v->capturing_index;
		virgo_cancel_capture(v);
		virgo_update_config_button_text(v, type, index);
		return 0;
	}
	case WM_KEYUP:
	case WM_SYSKEYUP:
		if (v && v->capturing_type != CAPTURE_TYPE_NONE) {
			return 0;
		}
		break;
	case WM_CHAR:
	case WM_SYSCHAR:
		if (v && v->capturing_type != CAPTURE_TYPE_NONE) {
			return 0;
		}
		break;
	case WM_KILLFOCUS:
		if (v && v->capturing_type != CAPTURE_TYPE_NONE) {
			virgo_cancel_capture(v);
		}
		break;
	case WM_CLOSE:
		DestroyWindow(hwnd);
		return 0;
	case WM_DESTROY:
		if (v) {
			unsigned i;
			virgo_cancel_capture(v);
			v->config_window = NULL;
			v->capturing_type = CAPTURE_TYPE_NONE;
			for (i = 0; i < NUM_DESKTOPS; i++) {
				v->switch_buttons[i] = NULL;
				v->move_buttons[i] = NULL;
			}
			v->button_prev = NULL;
			v->button_next = NULL;
		}
		SetWindowLongPtr(hwnd, GWLP_USERDATA, 0);
		return 0;
	}
	return DefWindowProc(hwnd, msg, wParam, lParam);
}

static void virgo_toggle_hotkeys(Virgo *v)
{
	v->handle_hotkeys = !v->handle_hotkeys;
	if (v->handle_hotkeys) {
		if (!virgo_register_all_hotkeys(v)) {
			MessageBox(NULL,
				   "Hotkey registration failed. Please adjust the hotkeys.",
				   "virgo", MB_ICONEXCLAMATION);
			virgo_show_config_window(v);
			v->handle_hotkeys = 0;
		}
	} else {
		virgo_unregister_all_hotkeys(v);
	}
}

static void virgo_init(Virgo *v)
{
	g_virgo = v;
	virgo_dwm_init();
	virgo_init_monitors(v);
	virgo_init_config_path(v);
	virgo_init_default_hotkeys(v);
	virgo_load_config(v);
	virgo_sync_pending_from_active(v);
	v->handle_hotkeys = 1;
	trayicon_init(v);
	trayicon_refresh(v);
	virgo_update_foreground_hook(v);
	if (!virgo_register_all_hotkeys(v)) {
		MessageBox(NULL,
			   "Hotkey registration failed. Please choose different hotkeys.",
			   "virgo", MB_ICONEXCLAMATION);
		v->handle_hotkeys = 0;
		virgo_show_config_window(v);
	}
}

static void virgo_deinit(Virgo *v)
{
	unsigned m, d;
	virgo_unregister_all_hotkeys(v);
	if (v->foreground_hook) {
		UnhookWinEvent(v->foreground_hook);
		v->foreground_hook = NULL;
	}
	for (m = 0; m < v->monitor_count; m++) {
		for (d = 0; d < NUM_DESKTOPS; d++) {
			windows_show(&v->monitors[m].desktops[d]);
			sb_free(v->monitors[m].desktops[d].windows);
			sb_free(v->monitors[m].z_order[d]);
			v->monitors[m].z_order[d] = NULL;
		}
	}
	trayicon_deinit(&v->trayicon);
	virgo_dwm_deinit();
	g_virgo = NULL;
}

static void virgo_move_to_desk(Virgo *v, unsigned desk)
{
	HWND hwnd;
	unsigned monitor_idx;
	MonitorState *state;
	if (desk >= NUM_DESKTOPS) {
		return;
	}
	virgo_update(v);
	hwnd = GetForegroundWindow();
	if (!hwnd || !is_valid_window(hwnd)) {
		return;
	}
	monitor_idx = virgo_monitor_from_hwnd(v, hwnd);
	state = &v->monitors[monitor_idx];
	if (state->current == desk) {
		return;
	}
	windows_del(&state->desktops[state->current], hwnd);
	windows_add(&state->desktops[desk], hwnd);
	virgo_set_window_visibility(v, hwnd, desk == state->current);
}

static void virgo_step_desktop(Virgo *v, int delta)
{
	unsigned monitor_idx;
	MonitorState *state;
	unsigned current;
	int step;
	if (!v || !v->monitor_count) {
		return;
	}
	monitor_idx = virgo_active_monitor(v);
	if (monitor_idx >= v->monitor_count) {
		monitor_idx = 0;
	}
	state = &v->monitors[monitor_idx];
	current = state->current;
	if (delta == 0) {
		return;
	}
	step = delta % NUM_DESKTOPS;
	if (step < 0) {
		step += NUM_DESKTOPS;
	}
	if (step == 0) {
		return;
	}
	virgo_switch_monitor_to_desk(v, monitor_idx,
		(current + (unsigned)step) % NUM_DESKTOPS, NULL);
}

static void virgo_go_to_desk(Virgo *v, unsigned desk)
{
	if (desk >= NUM_DESKTOPS) {
		return;
	}
	virgo_switch_monitor_to_desk(v, virgo_active_monitor(v), desk, NULL);
}

void __main(void) __asm__("__main");
void __main(void)
{
	Virgo v = {0};
	MSG msg;
	virgo_init(&v);
	while (GetMessage(&msg, NULL, 0, 0)) {
		if (msg.message == WM_HOTKEY) {
			if (msg.wParam == HOTKEY_ID_EXIT) {
				break;
			}
			if (msg.wParam == HOTKEY_ID_TOGGLE) {
				virgo_toggle_hotkeys(&v);
			} else if (msg.wParam == HOTKEY_ID_PREV) {
				virgo_step_desktop(&v, -1);
			} else if (msg.wParam == HOTKEY_ID_NEXT) {
				virgo_step_desktop(&v, 1);
			} else if (msg.wParam >= HOTKEY_ID_SWITCH_BASE &&
				msg.wParam < HOTKEY_ID_SWITCH_BASE + NUM_DESKTOPS) {
				virgo_go_to_desk(&v, msg.wParam - HOTKEY_ID_SWITCH_BASE);
			} else if (msg.wParam >= HOTKEY_ID_MOVE_BASE &&
				   msg.wParam < HOTKEY_ID_MOVE_BASE + NUM_DESKTOPS) {
				virgo_move_to_desk(&v, msg.wParam - HOTKEY_ID_MOVE_BASE);
			}
		} else {
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
	}
	virgo_deinit(&v);
	ExitProcess(0);
}
